4장. 부품으로 나누기 (모듈성)
출처: 『소프트웨어 설계의 결합 균형』(블라드 코노노프 지음, 제이펍 2026) | 원서: Balancing Coupling in Software Design (Manning) · 입문판·PDF 재구성 (4장)
코드는 분위기만 —
class·this·Save·Execute같은 말은 몰라도 됩니다. 각 줄 주석과 '비유'·'위험'만 봐도 충분해요.
0. 이 장의 새 단어 (0장에 없는 것 3개만)
0장 용어는 그대로 씁니다.
여기서는 4장에 처음 나오는 말 3개만 풀어 둡니다.
막히면 이 표로 돌아오세요.
모듈 / 모듈성 (Module / Modularity)
한 문장 뜻 — "혼자서도 돌아가고, 뺐다 꽂았다 할 수 있는 부품" 하나가 모듈. 시스템이 그런 부품들로 짜인 상태가 모듈성.
일상비유 — 레고 블록. 블록 하나하나가 모듈. 블록으로 짜인 구조물 전체가 모듈성을 가진 시스템.
한 줄 예 —
// 이 클래스 하나만 떼어 다른 곳에 꽂아도 돌아감 = 모듈
public class CustomerRepository // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
public void Save(object customer) { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
깊은 모듈 / 얕은 모듈 (Deep / Shallow Module)
한 문장 뜻 — 겉(쓰는 법)은 단순한데 속(하는 일)이 복잡하면 깊은 모듈, 겉과 속이 거의 똑같으면 얕은 모듈.
일상비유 — 깊은 모듈은 전자레인지. 버튼 하나 누르면 끝인데 안에서 일어나는 일은 복잡하다. 얕은 모듈은 "숫자 두 개 더하기" 버튼. 누르나 마나 직접 더하는 거랑 똑같다.
한 줄 예 —
static dynamic AddTwoNumbers(dynamic a, dynamic b) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
return a + b; // 결과를 호출자에게 돌려줍니다.
}
정보 은닉 (Information Hiding)
한 문장 뜻 — "바꿀 만한 결정"을 한 부품 안에 가둬서, 나중에 그 결정을 바꿔도 다른 부품은 안 건드리게 하는 원칙.
일상비유 — 콘센트. 발전소가 석탄이든 태양광이든 콘센트 모양은 안 바뀐다. "어떻게 만든 전기인가"를 콘센트가 숨겨 준다.
한 줄 예 —
repo.Save(customer); // DB가 MySQL인지 Mongo인지 숨김 — 호출하는 쪽은 모름
이런 적 있죠? (왜 이 장이 필요한가)
지금 잘 도는 시스템도, 내일 요구가 바뀌면 손봐야 합니다.
문제는 "손보기가 쉬운가"입니다.
한 군데 고치는데 열 군데가 줄줄이 깨지는 시스템이 있고,
한 군데만 딱 고치면 끝나는 시스템이 있습니다.
저자는 이걸 도구에 빗댑니다.
레고는 블록 하나 빼서 다른 걸 꽂으면 끝.
카메라는 렌즈만 갈아 끼우면 망원도 광각도 됩니다.
카메라 본체를 다시 살 필요가 없죠.
// 레고처럼 — 저장하는 부품(repo)만 갈아 끼우면 됨
service.Repo = mysql_repo; // 오늘은 MySQL
service.Repo = mongo_repo; // 내일은 Mongo — service 코드는 그대로
이렇게 "부품을 빼고·바꾸고·다시 꽂을 수 있게" 시스템을 짜는 것.
그게 바로 모듈성입니다.
"95%의 글이 모듈성이 좋다고만 말하고, 어떻게 만드는지는 거의 안 알려준다." — Myers(1979). 40년이 지나도 맞는 말이라, 이 장은 어떻게에 집중합니다.
이 장에서 딱 5가지만 (TL;DR)
- 모듈성 = 시스템을 "뺐다 꽂았다 할 수 있는 부품(모듈)"으로 짜서, 미래 변화에 적응하게 만드는 것.
- 모듈에는 세 얼굴이 있다 — 기능(보여줄 것)·로직(숨길 것)·맥락(환경에 대한 가정).
- 깊은 모듈(겉 단순·속 복잡)이 좋고, 얕은 모듈(겉=속)은 숨기는 게 없어 쓸모가 적다.
- 정보 은닉 — 바뀔 결정을 한 부품 안에 가두면, 고칠 때 그 부품만 손대면 된다.
- 모듈성은 부품 하나가 아니라 부품 사이의 연결(결합) 로 정해진다. 적게 알수록 모듈답다.
개념 1 — 모듈이란 무엇인가 (구성요소와 다른 점)
망가지는 장면
"이건 그냥 떼어다 다른 데 쓰면 되겠지" 했는데, 떼어 보니 딸려 오는 게 너무 많습니다.
이 부품은 저 부품 없이는 안 돌고, 저 부품은 또 다른 부품을 물고 있고….
결국 통째로 들고 와야 합니다. 이건 "부품"이긴 해도 "모듈"은 아닙니다.
비유 먼저
모든 시스템은 부품(구성요소)으로 돼 있습니다.
하지만 "혼자 떼어도 돌아가는 부품"만 모듈입니다.
레고 블록은 혼자 떼어도 멀쩡하지만, 케이크 안의 한 조각은 떼면 부서집니다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 레고 블록(모듈) | class Repository { void Save(...) { ... } } 떼어 써도 돎 |
거의 없음 — 갈아 끼우기 쉬움 |
| 케이크 한 조각(모듈 아님) | 다른 부품 5개 없으면 안 돎 | 떼면 부서짐 — 통째로만 옮김 |
정의
Myers(1979)는 모듈을 이렇게 정리했습니다. 다음 셋을 다 만족하면 모듈입니다.
- 독립적인 기능을 한다 (한 가지 일을 책임진다).
- 다른 데서 불러 쓸 수 있다.
- 따로 떼어 만들(컴파일할) 수 있다.
즉 모듈은 "구성요소의 한 종류" 입니다. 모든 모듈은 구성요소지만, 모든 구성요소가 모듈은 아닙니다.
중요한 건 부품의 겉모습(파일이냐 서비스냐)이 아니라, 떼어 쓸 수 있느냐입니다.
그래서 서비스도, 패키지도, 클래스도, 함수 하나도 모두 모듈이 될 수 있습니다.
예시 폭격
(1) 완성 예 (worked) — 모듈인 것 vs 아닌 것
// 모듈 O — 혼자 불러 쓸 수 있고, 한 가지 일을 책임짐
static dynamic CalculateInvoice(dynamic order) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
return sum(item.Price for item in order.Items); // 결과를 호출자에게 돌려줍니다.
}
// 모듈 X — 밖에서 못 부름( _ 로 숨김), 혼자선 의미 없음
static dynamic InternalTaxHelper(dynamic x) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
return x * 0.1; // 결과를 호출자에게 돌려줍니다.
}
(2) 부분 완성 — 빈칸을 채워 보세요
// 아래 둘 중 "모듈"인 것은? (혼자 떼어 다른 프로젝트에 꽂을 수 있는 쪽)
public class EmailSender // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
public void Send(string to, string body) { /* 구현 생략 */; } // ← (A)
}
static dynamic FormatHtml(dynamic raw) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
/* 구현 생략 */ // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
// 정답: (A). (B)는 EmailSender 속에서만 쓰여 혼자 못 떼어 냄.
(3) 독립 적용 — 내 코드에서
"이 부품, 다른 프로젝트에 그냥 복사해 넣어도 돌아가나?"
"예"면 모듈, "딸려 오는 게 많아 안 됨"이면 아직 모듈이 아닙니다.
미니 시나리오
예: 레고 구조물 전체가 "시스템", 블록 하나가 "모듈".
새 모양을 만들 때 블록을 다시 깎지 않고 다시 조립만 하면 됩니다.
이게 모듈성의 힘입니다.
개념 2 — 모듈의 세 얼굴: 기능·로직·맥락
망가지는 장면
"이 부품 어떻게 쓰는 거야?"라고 물었더니, 답이 "소스 코드 다 읽어 봐"입니다.
쓰는 법(겉)과 만든 법(속)이 안 나뉘어 있으면, 쓰려는 사람이 속까지 다 알아야 합니다.
반대로 "이 부품, 어떤 환경에서 돌아가는데?"를 아무도 안 적어 놨다가, 다른 데로 옮기자 조용히 깨집니다.
비유 먼저
식당으로 보면 쉽습니다.
- 기능 = 메뉴판. 손님에게 "이건 이렇게 주문하세요"라고 보여주는 약속.
- 로직 = 주방. 어떻게 만드는지는 숨긴다. 손님이 알 필요 없다.
- 맥락 = 가게 사정. "현금만 받아요", "포장은 안 돼요" 같은 환경 가정. 메뉴판엔 잘 안 적혀 있지만 알아야 한다.
| 얼굴 | 비유 | 코드 | 위험 |
|---|---|---|---|
| 기능 | 메뉴판(보여줌) | void Save(Customer customer) { ... } |
너무 많이 보여주면 바꾸기 어려움 |
| 로직 | 주방(숨김) | this.connection.Execute("INSERT ...") |
밖으로 새면 → 다 깨짐 |
| 맥락 | 가게 사정(가정) | ".NET 8 이상 필요" | 안 적으면 → 옮길 때 조용히 깨짐 |
정의
- 기능 (Function) — 모듈이 바깥에 보여주는 목표. 공개 인터페이스로 드러난다.
- 로직 (Logic) — 그 기능을 어떻게 만드는지. 다른 부품에게 숨겨야 한다.
- 맥락 (Context) — 모듈이 돌아가려면 환경에 대해 가정하는 것들. 잘 안 드러나서 더 위험하다.
저자는 레고·카메라로 이 셋을 보여 줍니다.
레고 블록 — 기능: 다른 블록과 끼움(요철·구멍). 로직: 무게 버티는 소재. 맥락: 어린이 장난감(진짜 집 짓는 용도 아님).
카메라 렌즈 — 기능: 특정 초점·조리개로 촬영. 로직: 렌즈 내부 광학 작동. 맥락: 지원되는 카메라 모델, 자동 초점 여부.
예시 폭격
(1) 완성 예 (worked) — 세 얼굴이 잘 나뉜 경우
public class CustomerRepository // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
"""; // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
맥락: DB 연결이 미리 열려 있어야 함 / 저장 전 검증을 끝냈다고 가정; // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
"""; // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
public dynamic Save(dynamic customer) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
this.Conn.Execute(; // 로직 — 숨김
"INSERT INTO customers /* 구현 생략 */", (/* 구현 생략 */); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
}
(2) 부분 완성 — 이 코드의 세 얼굴을 짚어 보세요
public class EmailNotifier // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
public void SendWelcome(string email, string name) { /* 구현 생략 */; } // 기능 = (?)
// 내부적으로 SendGrid 사용, 초당 100통 넘으면 막힘
// → 로직 = (?)
// → 맥락 = (?)
}
// 정답: 기능 = send_welcome(가입 환영 메일 발송)
// 로직 = SendGrid 호출·재시도 (숨김)
// 맥락 = "초당 100통 제한" (대량 발송 짤 때 꼭 알아야 함)
(3) 독립 적용 — 위험 신호
맥락이 코드 어디에도, 문서 어디에도 안 적혀 있다면?
그건 "조용한 결합"입니다. 옮기거나 바꿀 때 갑자기 터집니다. 맥락은 글로 끌어올려 두세요.
미니 시나리오
예: 콘센트는 기능(220V 구멍)을 보여주고, 발전 방식(로직)을 숨깁니다.
하지만 "여기는 220V"라는 맥락을 모르고 110V 기기를 꽂으면 터집니다.
맥락을 안 적으면 이런 일이 생깁니다.
개념 3 — 깊은 모듈 vs 얕은 모듈
망가지는 장면
부품을 잔뜩 만들었는데, 막상 쓰려니 부품마다 사용법이 너무 많습니다.
함수 이름만 봐도 "아, 이건 그냥 안에서 하는 일 그대로네" 싶습니다.
이런 부품은 있으나 마나입니다. 숨겨 주는 게 없으니까요.
비유 먼저
오스터하우트(2018)는 모듈을 사각형으로 그려 보라고 합니다.
- 윗변(좁을수록 좋음) = 쓰는 법(기능).
- 사각형 넓이(넓을수록 좋음) = 안에서 하는 일(로직).
윗변이 좁고 속이 넓으면 "깊은 모듈". 많이 숨겨 줍니다.
윗변과 속이 거의 같으면 "얕은 모듈". 숨기는 게 없습니다.
전자레인지는 깊은 모듈(버튼 하나, 속은 복잡), "숫자 더하기" 함수는 얕은 모듈(겉=속).
| 비유 | 코드 | 위험 |
|---|---|---|
| 전자레인지(깊음) | repo.Save(customer) 속은 검증·트랜잭션·캐시 |
안전 — 속을 몰라도 됨 |
| 숫자 더하기(얕음) | int Add(int a, int b) => a + b |
쓸모 적음 — 직접 쓰는 거랑 같음 |
정의
- 깊은 모듈 — 좁은(간결한) 사용법 뒤에 복잡한 속을 숨긴 모듈. 쓰는 사람은 속을 몰라도 된다. 좋다.
- 얕은 모듈 — 사용법과 속이 거의 같은 모듈. 숨기는 게 없어 가치가 거의 없다. 피하자.
예시 폭격
(1) 완성 예 (worked) — 얕은 것 vs 깊은 것
// 얕음 — 인터페이스가 곧 구현. 숨긴 게 없음
static dynamic AddTwoNumbers(dynamic a, dynamic b) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
return a + b; // 결과를 호출자에게 돌려줍니다.
}
// 깊음 — 겉은 save() 한 줄, 속은 복잡
public class CustomerRepository // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
public dynamic Save(dynamic customer) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
this.Validate(customer); // 검증
using (this.Conn.BeginTransaction()) // 트랜잭션
this.Conn.Execute("UPSERT /* 구현 생략 */", (/* 구현 생략 */)); // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
this.UpdateSearchIndex(customer); // 인덱스
}
}
// 쓰는 쪽이 아는 건 save(customer) 하나뿐
(2) 부분 완성 — 이 클래스는 깊은가 얕은가?
public class CustomerDTO // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
private string name; // 실제 값은 객체 안에 숨겨 둡니다.
public string GetName() // 한 줄짜리 규칙도 호출자가 알게 되는 약속입니다.
{
return this.name; // 이름 값을 그대로 돌려줍니다.
}
public void SetName(string name) // 이름을 바꾸는 공개 행동입니다.
{
this.name = name; // 검증이나 숨은 규칙 없이 값만 넣습니다.
}
}
// 검증도, 숨긴 일도 없음 → (얕음 / 깊음) 중 ?
// 정답: 얕음. 구조체에 껍데기만 씌운 꼴.
(3) 독립 적용 — Unix 파일 다루기
예: open / read / write / close — 사용법은 네 개뿐입니다.
하지만 속은 권한 검사, 디스크 블록 배치, 캐시 관리 등 엄청 복잡합니다.
쓰는 사람은 그 중 하나도 몰라도 됩니다. 이게 깊은 모듈의 본보기입니다.
미니 시나리오
예: 깊은 모듈도 한계가 있습니다.
같은 규칙을 두 모듈에 똑같이 구현해 두면, 규칙이 바뀔 때 둘 다 고쳐야 합니다.
하나만 고치면 둘이 어긋납니다.
그래서 "깊게" 만드는 것만으로는 부족하고, 중복 없이 한 곳에만 두는 것도 필요합니다.
개념 4 — 정보 은닉과 추상화 (파나스의 원칙)
망가지는 장면
"DB만 바꾸려는데 화면 코드까지 손봐야 해?"
화면 코드가 DB 컬럼 이름을 직접 알고 있어서, 컬럼명 하나 바꾸자 화면이 깨집니다.
"바꿀 만한 결정"이 한 곳에 안 갇혀 있고 여기저기 새어 나가 있던 겁니다.
비유 먼저
데이비드 파나스(1971)는 "모듈은 책임을 맡은 단위"라며, 좋은 모듈은 결정을 숨기는 모듈이라고 했습니다.
콘센트가 좋은 예입니다. 발전 방식(석탄·태양광)이 바뀌어도 콘센트 모양은 그대로.
"어떻게 만든 전기인가"라는 바뀔 결정을 콘센트가 숨겨 줍니다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 콘센트(결정 숨김) | repo.Save(customer) |
안전 — DB 바꿔도 호출부 그대로 |
| 노출된 배선(결정 샘) | db.execute("INSERT INTO ... items_json") |
컬럼명 바꾸면 호출부 다 깨짐 |
정의
- 정보 은닉 (Information Hiding) — 바뀔 만한 결정을 한 모듈 안에 가두는 원칙. 결정을 바꿔도 그 모듈만 고치면 된다.
- 추상화 (Abstraction) — 여러 대상의 공통점만 남기고 나머지 세부는 지운 것. "자동차"라는 말은 색·모델을 지우고 "4바퀴·엔진·운송"만 남긴 추상화다.
파나스가 2023년 저자에게 보낸 편지에서 실패담을 들려줍니다.
"1960년대 후반, 관리자들은 일을 부분으로 나누면 '잘 맞아떨어질' 거라 기대했다. 하지만 시스템은 잘 안 돌았고, 한 군데 바꾸면 여러 모듈이 다 영향받았다. 이유: 나눌 때 '잘못 나눴기' 때문. 부품 사이 인터페이스가 너무 복잡했다." — Parnas(2023)
데이크스트라(1972)는 추상화의 목적을 이렇게 못 박습니다.
"추상화의 목적은 모호하게 만드는 게 아니라, 절대적으로 정확할 수 있는 새 의미 수준을 만드는 것이다."
추상화는 넓이의 균형이 중요합니다. 너무 넓으면("탈것") 정작 필요한 게 가려지고, 너무 좁으면("빨간 테슬라") 재사용이 안 됩니다.
예시 폭격
(1) 완성 예 (worked) — 결정이 새는 코드 → 의도만 남기기
// before — DB 결정이 밖으로 샘 (MySQL이라는 사실을 호출부가 앎)
public class CustomerService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
private readonly MySqlRepository repo; // 구체 DB 타입을 직접 알기 때문에 결합이 강합니다.
public CustomerService(MySqlRepository repo) // 생성자에서 MySQL 저장소를 직접 받습니다.
{
this.repo = repo; // 이 서비스는 MySQL 저장소 없이는 동작하기 어렵습니다.
}
public dynamic Register(dynamic c) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
this.repo.ExecuteSql("INSERT INTO customers /* 구현 생략 */"); // SQL까지 알아 결정이 밖으로 샙니다.
}
}
// after — 의도만 남김 ("저장한다"만 알면 됨)
public class CustomerService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
private readonly ICustomerRepository repo; // 인터페이스만 알면 구체 DB를 몰라도 됩니다.
public CustomerService(ICustomerRepository repo) // 저장소 약속을 생성자로 받습니다.
{
this.repo = repo; // 호출자는 "저장 가능하다"는 사실만 기억합니다.
}
public dynamic Register(dynamic c) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
this.repo.Save(c); // MySQL? Mongo? 모름. 안 깨짐
}
}
(2) 부분 완성 — 어느 쪽이 더 추상화가 잘됐나?
this.Repo.ExecuteSql("INSERT /* 구현 생략 */"); // (A) SQL을 안다
this.Repo.Save(customer); // (B) "저장한다"만 안다
// 결정(SQL/DB)을 더 잘 숨긴 쪽은? → 정답: (B)
(3) 독립 적용 — 추상화 넓이 고르기
"자동차"를 "탈것"으로 부르면? 오토바이·배·비행기까지 다 떠안아야 합니다.
오토바이를 진짜로 함께 다룰 일이 없다면, 그냥 "자동차"가 정확하고 적당한 추상화입니다.
미니 시나리오
예: 저장소를 MySQL → PostgreSQL → Mongo → (테스트용) 메모리 가짜 객체로 바꿔도,
save(customer)만 보고 쓰던 호출부는 한 줄도 안 바뀝니다.
결정을 한 곳에 가둔 덕분입니다.
개념 5 — 모듈성을 망치는 것들: 큰 진흙덩이와 누출 추상화
망가지는 장면
함수 하나가 DB도 알고, 이메일 서버도 알고, 결제 API도 압니다.
DB 컬럼명 하나 바꿨더니 그 함수가 깨지고, 옆 함수도 같은 컬럼을 알고 있어 같이 깨집니다.
지식이 온 사방에 퍼져 있어, 어디를 고쳐야 할지 머리가 꽉 찹니다.
비유 먼저
저자는 이런 코드를 큰 진흙덩이 (Big Ball of Mud) 라고 부릅니다(Foote and Yoder 1997).
"체계 없이 뒤죽박죽 이어 붙인 코드 정글. 정보가 멀리 떨어진 곳끼리 무분별하게 공유되고, 중요한 정보가 곳곳에 전역화·중복돼 있다."
또 하나의 적은 누출 추상화 (Leaking Abstraction) 입니다.
겉으론 깔끔한 척하지만, 제대로 쓰려면 속을 알아야 하는 부품입니다. 속이 새고 있는 거죠.
| 비유 | 코드 | 위험 |
|---|---|---|
| 큰 진흙덩이 | 한 함수가 DB·이메일·결제를 다 앎 | 한 곳 바꾸면 줄줄이 깨짐 |
| 누출 추상화 | cache.get() 이 null만 줌(만료인지 없음인지 모름) |
제대로 쓰려면 속을 알아야 함 |
정의
- 큰 진흙덩이 — 지식이 사방에 무분별하게 퍼진, 모듈성의 정반대 상태.
- 누출 추상화 — 숨겨야 할 속사정이 밖으로 새어, 소비자가 속을 알아야만 올바르게 쓸 수 있는 상태.
모듈식 설계가 복잡성을 누르는 두 방식이 있습니다.
- 우발적 복잡성 제거 — 잘못 짜서 생긴 복잡함을 없앤다.
- 필수적 복잡성 관리 — 없앨 수 없는 복잡함은 한 모듈에 가둬 전체로 새지 않게 한다.
그리고 과유불급 — 모든 변화를 다 받아내려 하면 인터페이스가 비대해지고 오히려 복잡해집니다.
"재사용성이 높을수록 유용성은 떨어진다." 그래서 합리적인 변화에만 열어 둡니다.
예시 폭격
(1) 완성 예 (worked) — 진흙덩이 → 책임 나누기
// before — 한 함수가 모든 걸 다 앎 (진흙덩이)
static dynamic CreateOrder() // 이 메서드의 입력과 출력이 공개 약속입니다.
{
db.Execute("INSERT INTO orders (user_id, items_json) /* 구현 생략 */", (/* 구현 생략 */)); // DB
smtp.Sendmail("order@co.Com", body["email"], "주문 완료"); // 이메일
payment_api.Charge(body["card_number"], body["amount"]); // 결제
}
// after — 각 책임을 모듈 안에 가둠
static dynamic CreateOrder() // 이 메서드의 입력과 출력이 공개 약속입니다.
{
var order = order_service.Create(command); // 비즈니스 로직 숨김
notification_service.NotifyCreated(order); // 이메일 방식 숨김
// 핸들러는 "무엇을"만 알고 "어떻게"는 모름
}
(2) 부분 완성 — 어디가 누출인가?
public class CacheService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
public dynamic Get(dynamic key) // 이 메서드의 입력과 출력이 공개 약속입니다.
{
return this.Redis.Get(key); // 만료돼도 null, 없어도 null
}
}
// 소비자: null을 받았는데... 만료된 건가? 처음부터 없던 건가?
// → 구분하려면 Redis 속사정을 알아야 함 = (누출 / 정상) ?
// 정답: 누출. "만료"와 "없음"을 인터페이스가 구분해 주지 않음.
(3) 독립 적용 — 진흙덩이 냄새 맡기
한 함수가 DB 컬럼명·외부 API·이메일 형식을 동시에 알고 있다면 진흙덩이 신호입니다.
그 지식들을 각각 제 모듈로 옮기면, 고칠 곳이 한 군데로 줄어듭니다.
미니 시나리오
예: OrderService와 ReportService가 둘 다 items_json 컬럼명을 직접 안다고 합시다.
컬럼명을 items로 바꾸면 둘 다 깨집니다.
그 지식을 OrderRepository 한 곳에 가두면, 컬럼명이 바뀌어도 그 한 곳만 고치면 됩니다.
이게 "지식을 모듈에 가둔다"의 실제 모습입니다.
개념 6 — 결국 모듈성은 '연결'로 정해진다
망가지는 장면
부품 하나하나는 흠잡을 데 없이 잘 만들었습니다.
그런데 부품들을 이어 보니, 한 군데 건드리면 엉뚱한 데가 터집니다.
부품이 아니라 부품 사이의 선이 문제였습니다.
비유 먼저
앨런 케이는 말했습니다.
"객체 지향의 큰 아이디어는 클래스가 아니라 메시징이다."
즉 상자(부품)가 아니라 상자를 잇는 화살표(상호작용) 가 핵심이라는 뜻입니다.
| 비유 | 코드 | 위험 |
|---|---|---|
| 상자만 봄 | 부품 하나하나만 잘 만듦 | 이어 보면 깨질 수 있음 |
| 화살표를 봄 | 부품 사이 공유 지식을 줄임 | 적게 알수록 모듈답고 안전 |
정의
- 결합 — 부품 사이에 공유되는 지식을 정하는 것. (0장 용어) 많이 공유하면 복잡, 적게 공유하면 모듈답다.
- 응집력 (Cohesion) — 한 모듈 안의 요소들이 서로 얼마나 가까운가. 한 모듈이 한 가지 목적에 집중할수록 높다. 흔히 "좋은 결합"이라 부른다.
정리하면 이렇습니다.
- 결합은 부품 사이의 관계 → 적게 공유할수록 좋다.
- 응집력은 부품 안의 관계 → 가까이 모일수록 좋다.
모듈성은 부품 하나로는 절대 평가할 수 없습니다. 연결을 봐야 합니다.
응집력을 코드로 보면 이렇습니다.
// 응집력 낮음 — 한 클래스에 안 어울리는 일들이 뒤섞임
public class UserStuff // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
public void SaveUser(object u) { /* 구현 생략 */; } // 저장
public void SendEmail(object u) { /* 구현 생략 */; } // 메일 — 결이 다름
public void DrawChart(object u) { /* 구현 생략 */; } // 차트 — 또 다른 결
}
// 응집력 높음 — 한 가지 목적(저장)에만 집중
public class UserRepository // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
public void Save(object u) { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
public void Find(int id) { /* 구현 생략 */; } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
// 메일·차트는 각자 제 모듈로 분리
예시 폭격
(1) 완성 예 (worked) — 지식을 흩뿌림 → 한 곳에 모음
// before — 두 모듈이 같은 컬럼명을 각자 앎 (복잡성 결합)
public class OrderService // 주문 흐름을 조정하는 서비스입니다. 어떤 협력 객체를 아는지가 결합을 만듭니다.
{
public void Place(object o) { db.Execute("INSERT INTO orders (items_json) /* 구현 생략 */"); } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
public class ReportService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
public void Summary() { db.Execute("SELECT items_json FROM orders /* 구현 생략 */"); } // 이 호출은 두 부품 사이의 연결을 보여 줍니다.
}
// items_json → items 로 바꾸면 둘 다 깨짐
// after — 지식을 OrderRepository 한 곳에 모음 (모듈성 결합)
public class OrderService // 주문 흐름을 조정하는 서비스입니다. 어떤 협력 객체를 아는지가 결합을 만듭니다.
{
public void Place(object o) { this.Repo.Save(o); } // 컬럼명 모름
}
public class ReportService // 이 타입은 예제에서 책임과 결합의 경계를 보여 줍니다.
{
public dynamic Summary() // 한 줄짜리 규칙도 호출자가 알게 되는 약속입니다.
{
return this.Repo.MonthlyTotals(/* 구현 생략 */); // 계산 결과를 호출자에게 돌려줍니다.
}
}
// 컬럼명 바뀌면 OrderRepository 한 곳만 수정
(2) 부분 완성 — 어느 쪽이 변경에 강한가?
// (A) 화면·보고서·서비스가 각자 SQL을 직접 씀
// (B) 모두 repo.Save() / repo.find() 만 부름
// 컬럼명 하나 바꿀 때 고칠 곳이 적은 쪽은? → 정답: (B)
(3) 독립 적용 — 같은 지식이 두 곳에?
같은 컬럼명·같은 규칙을 두 모듈이 따로 안다면, 그 지식을 한 모듈로 끌어모으세요.
연결이 가늘어지고, 고칠 곳이 줄어듭니다.
미니 시나리오
쇼핑몰의 ProductCatalogService가 가격 계산 공식을 직접 들고 있으면,
할인 정책이 바뀔 때 카탈로그까지 고쳐야 합니다.
대신 pricing.get_price(...)로 물어보기만 하면, 정책이 바뀌어도 PricingService 한 곳만 고칩니다.
단순 규칙 (지금은 이거 하나면 충분)
부품끼리 서로 아는 게 가장 적은 방식이 기본 안전판입니다.
- 모듈을 만들 땐 세 얼굴을 챙기세요 — 기능(보여줄 것)·로직(숨길 것)·맥락(가정).
- 사용법(겉)은 좁게, 속은 깊게. 숨길수록 좋습니다.
- 같은 지식을 두 곳이 알고 있으면, 한 곳에 모으세요.
한 걸음 더 ▸ (지금 몰라도 됨)
"무조건 느슨하게 = 정답"은 아닙니다.
너무 추상화하면(예: "탈것") 정작 필요한 정보까지 가려질 수 있습니다.
그래서 "적당한 넓이"를 고르는 균형이 필요한데, 이건 더 깊은 주제라 지금은 "적게 알수록 안전" 하나만 들고 가면 됩니다.
정리
모듈성은 시스템을 뺐다 꽂았다 할 수 있는 부품으로 짜서 미래 변화에 적응하게 만드는 것입니다.
좋은 모듈은 겉(사용법)은 좁고 속(구현)은 깊으며, 바뀔 결정을 자기 안에 가둡니다(정보 은닉).
그리고 모듈성은 부품 하나가 아니라 부품 사이의 연결(결합) 로 정해집니다 — 적게 알수록 모듈답습니다.
더 해보기
- 누출 추상화의 원전: The Law of Leaky Abstractions (Joel on Software, 2002) — 검증 2026-05-26.
- 큰 진흙덩이 원전: Big Ball of Mud (laputan.org) — 검증 2026-05-26.
다음 장 예고 — 다음 장에서는 이 '연결(결합)'을 처음으로 재는 방법을 봅니다. 지금은 "적게 알수록 안전" 하나만 들고 가면 됩니다.
연습문제
- 설명.
부품으로 나누기 (모듈성)의 핵심을 처음 듣는 사람에게 한 문장으로 설명하라. - 구분. 두 개념(
깊은 모듈,얕은 모듈)을 실제 예시 하나로 구분하라. - 적용. 내 프로젝트나 학습 노트에서 이 장의 개념을 적용해 작게 개선할 지점을 하나 고르라.
부록 A. 쉬운 용어 사전
| 한글 용어 | 원문 영문명 | 아주 쉬운 뜻 | 이 장에서 나온 위치 |
|---|---|---|---|
| 깊은 모듈 | Deep Module | 작은 인터페이스 뒤에 많은 일을 숨기는 모듈. | 부록 B와 본문 예시 |
| 얕은 모듈 | Shallow Module | 겉으로만 나뉘었고 실제로는 별로 숨기지 못한 모듈. | 부록 B와 본문 예시 |
| 정보 은닉 | Information Hiding | 바깥에서 몰라도 되는 내부 사정을 숨기는 설계 원칙. | 부록 B와 본문 예시 |
| 추상화 | Abstraction | 구체적인 속사정보다 필요한 약속과 의미만 남기는 일. | 부록 B와 본문 예시 |
부록 B. 헷갈리는 개념 비교표
| A | B | 구분 포인트 |
|---|---|---|
| 깊은 모듈 | 얕은 모듈 | 깊은 모듈은 작은 인터페이스로 많은 일을 숨기고, 얕은 모듈은 겉만 나눈다. |
| 정보 은닉 | 추상화 | 정보 은닉은 숨기는 일, 추상화는 필요한 약속만 남기는 일이다. |
부록 C. 더 읽을 자료
- 이 장의
더 해보기섹션 — 이미 모아 둔 공식 문서나 실습 링크가 있으면 여기서 먼저 확인한다. - 같은 책의
0장 한눈에 보기— 용어가 막히면 0장의 용어집과 개념 척추로 돌아간다. - 원본 딥다이브판 같은 장 — 입문판을 읽고 큰 흐름이 잡힌 뒤 세부 논리를 더 깊게 확인한다.
- 이 장의
flashcards.json— 읽은 직후 질문만 보고 답을 떠올리는 회상 연습에 쓴다.
부록 D. 연습문제 풀이
- 설명 예시.
부품으로 나누기 (모듈성)는 변경이 어디로 번지는지 보고, 필요한 연결과 줄여야 할 연결을 구분하게 해 주는 장이다. 중요한 것은 용어를 외우는 것이 아니라, 이 개념이 어떤 입력·부품·결정에 영향을 주는지 말로 풀어 보는 것이다. - 구분 예시. 두 개념(
깊은 모듈,얕은 모듈)의 차이는 이렇게 잡으면 된다. 깊은 모듈은 작은 인터페이스로 많은 일을 숨기고, 얕은 모듈은 겉만 나눈다. 실제 사례를 볼 때는 목적, 입력, 실패했을 때의 증상을 따로 적어 보면 헷갈리지 않는다. - 적용 예시. 가장 작은 개선부터 고른다. 예를 들어 이름을 더 분명히 하거나, 평가 기준을 한 줄 추가하거나, 직접 알 필요 없는 내부 정보를 감추는 식으로 시작한다. 한 번에 크게 갈아엎는 것보다 작은 변경 하나를 확인하며 진행하는 쪽이 입문 단계에 맞다.
클릭하거나 Space를 눌러 뒤집기